/* SPDX-FileCopyrightText: 2020-2023 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup GHOST
 */

#include "GHOST_WindowWayland.hh"
#include "GHOST_SystemWayland.hh"
#include "GHOST_WaylandUtils.hh"
#include "GHOST_WindowManager.hh"
#include "GHOST_utildefines.hh"

#include "GHOST_Event.hh"

#include "GHOST_ContextNone.hh"
#ifdef WITH_OPENGL_BACKEND
#  include "GHOST_ContextEGL.hh"
#endif
#ifdef WITH_VULKAN_BACKEND
#  include "GHOST_ContextVK.hh"
#endif

#include <wayland-client-protocol.h>

#ifdef WITH_OPENGL_BACKEND
#  ifdef WITH_GHOST_WAYLAND_DYNLOAD
#    include <wayland_dynload_egl.h>
#  endif
#  include <wayland-egl.h>
#endif

#include <algorithm> /* For `std::find`. */

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
#  ifdef WITH_GHOST_WAYLAND_DYNLOAD
#    include <wayland_dynload_libdecor.h>
#  endif
#  include <libdecor.h>

#  ifdef WITH_VULKAN_BACKEND
#    include <unistd.h> /* For `ftruncate`. */
#  endif
#endif

/* Generated by `wayland-scanner`. */
#include <fractional-scale-v1-client-protocol.h>
#include <viewporter-client-protocol.h>
#include <xdg-activation-v1-client-protocol.h>
#include <xdg-decoration-unstable-v1-client-protocol.h>
#include <xdg-shell-client-protocol.h>

#include <atomic>

#include <cstring>  /* For `memcpy`. */
#include <malloc.h> /* For `malloc_usable_size`. */

/* Logging, use `ghost.wl.*` prefix. */
#include "CLG_log.h"

/**
 * LIBDECOR support committing a window-configuration in the main-thread that was
 * handled in a non-main-thread.
 *
 * This is needed for proper order of operations as the commit needs to occur after the buffer
 * is resized (something that can't be done from a non-main thread).
 * Without this, the window-frame can get out of sync with its contents and resizing is slow.
 *
 * Eventually LIBDECOR should support copying & freeing a configuration, even when it does,
 * older versions of the library will to be supported and the workaround will need to be kept.
 * See: https://gitlab.freedesktop.org/libdecor/libdecor/-/issues/64
 */
#ifdef USE_EVENT_BACKGROUND_THREAD
#  ifdef WITH_GHOST_WAYLAND_LIBDECOR
#    ifdef HAVE_MALLOC_USABLE_SIZE
#      define USE_LIBDECOR_CONFIG_COPY_WORKAROUND
#    endif
#  endif
#endif

#ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
static libdecor_configuration *ghost_wl_libdecor_configuration_copy(
    const libdecor_configuration *configuration);
static void ghost_wl_libdecor_configuration_free(libdecor_configuration *configuration);
#endif

static const xdg_activation_token_v1_listener *xdg_activation_listener_get();

static constexpr size_t base_dpi = 96;

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/* Access `use_libdecor` in #GHOST_SystemWayland. */
#  define use_libdecor GHOST_SystemWayland::use_libdecor_runtime()
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
struct GWL_LibDecor_Window {
  libdecor_frame *frame = nullptr;

  /**
   * Defer calling #libdecor_frame_commit.
   * \note Accessing the members must lock on `win->frame_pending_mutex`.
   */
  struct {
    /** When set, ACK configure is expected. */
    bool ack_configure = false;
    /** The new size to use. */
    int size[2] = {0, 0};
    libdecor_configuration *configuration = nullptr;

#  ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
    bool configuration_needs_free = false;
#  endif
  } pending;

  /** The window has been configured (see #xdg_surface_ack_configure). */
  bool initial_configure_seen = false;
};

static void gwl_libdecor_window_destroy(GWL_LibDecor_Window *decor)
{
#  ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
  if (decor->pending.configuration_needs_free) {
    ghost_wl_libdecor_configuration_free(decor->pending.configuration);
  }
#  endif /* USE_LIBDECOR_CONFIG_COPY_WORKAROUND */
  libdecor_frame_unref(decor->frame);
  delete decor;
}
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */

struct GWL_XDG_Decor_Window {
  xdg_surface *surface = nullptr;
  zxdg_toplevel_decoration_v1 *toplevel_decor = nullptr;
  xdg_toplevel *toplevel = nullptr;
  enum zxdg_toplevel_decoration_v1_mode mode = (enum zxdg_toplevel_decoration_v1_mode)0;

  /**
   * Defer calling #xdg_surface_ack_configure.
   * \note Accessing the members must lock on `win->frame_pending_mutex`.
   */
  struct {
    /** When set, ACK configure is expected. */
    bool ack_configure = false;
    /** The serial to pass to ACK configure. */
    uint32_t ack_configure_serial = 0;
  } pending;

  /** The window has been configured (see #xdg_surface_ack_configure). */
  bool initial_configure_seen = false;
};

static void gwl_xdg_decor_window_destroy(GWL_XDG_Decor_Window *decor)
{
  if (decor->toplevel_decor) {
    zxdg_toplevel_decoration_v1_destroy(decor->toplevel_decor);
  }
  xdg_toplevel_destroy(decor->toplevel);
  xdg_surface_destroy(decor->surface);
  delete decor;
}

/* -------------------------------------------------------------------- */
/** \name Rounding Utilities
 * \{ */

static void gwl_round_int_by(int *value_p, const int round_value)
{
  GHOST_ASSERT(round_value > 0, "Invalid rounding value!");
  *value_p = (*value_p / round_value) * round_value;
}

static void gwl_round_int2_by(int value_p[2], const int round_value)
{
  GHOST_ASSERT(round_value > 0, "Invalid rounding value!");
  value_p[0] = (value_p[0] / round_value) * round_value;
  value_p[1] = (value_p[1] / round_value) * round_value;
}

/**
 * Return true if the value is already rounded by `round_value`.
 */
static bool gwl_round_int_test(int value, const int round_value)
{
  return value == ((value / round_value) * round_value);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Window-Viewport/Wayland to/from Scale Conversion
 * \{ */

struct GWL_WindowScaleParams {
  bool is_fractional = false;
  /**
   * When fractional:
   * Scale is multiplied by #FRACTIONAL_DENOMINATOR.
   * Otherwise scale is an integer.
   */
  wl_fixed_t scale = 0;
};

wl_fixed_t gwl_window_scale_wl_fixed_to(const GWL_WindowScaleParams &scale_params,
                                        wl_fixed_t value)
{
  if (scale_params.is_fractional) {
    return (value * scale_params.scale) / FRACTIONAL_DENOMINATOR;
  }
  return value * scale_params.scale;
}
wl_fixed_t gwl_window_scale_wl_fixed_from(const GWL_WindowScaleParams &scale_params,
                                          wl_fixed_t value)
{
  if (scale_params.is_fractional) {
    return (value * FRACTIONAL_DENOMINATOR) / scale_params.scale;
  }
  return value / scale_params.scale;
}

int gwl_window_scale_int_to(const GWL_WindowScaleParams &scale_params, int value)
{
  return wl_fixed_to_int(gwl_window_scale_wl_fixed_to(scale_params, wl_fixed_from_int(value)));
}
int gwl_window_scale_int_from(const GWL_WindowScaleParams &scale_params, int value)
{
  return wl_fixed_to_int(gwl_window_scale_wl_fixed_from(scale_params, wl_fixed_from_int(value)));
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window
 * \{ */

#ifdef USE_EVENT_BACKGROUND_THREAD

enum eGWL_PendingWindowActions {
  /**
   * The state of the window frame has changed, apply the state from #GWL_Window::frame_pending.
   */
  PENDING_WINDOW_FRAME_CONFIGURE = 0,
  /**
   * The DPI for a monitor has changed or the monitors (outputs)
   * this window is visible on may have changed. Recalculate the windows scale.
   */
  PENDING_OUTPUT_SCALE_UPDATE,

  /**
   * Workaround for a bug/glitch in WLROOTS based compositors (RIVER for e.g.).
   * Deferring the scale update one even-loop cycle resolves a bug
   * where the output enter/exit events cause the surface buffer being an invalid size.
   *
   * While these kinds of glitches might be ignored in some cases,
   * this caused newly created windows to immediately loose the connection to WAYLAND
   * (crashing from a user perspective).
   */
  PENDING_OUTPUT_SCALE_UPDATE_DEFERRED,

  /**
   * The surface needs a commit to run.
   * Use this to avoid committing immediately which can cause flickering when other operations
   * have not yet been performed - such as refreshing the window size.
   */
  PENDING_WINDOW_SURFACE_COMMIT,

};
#  define PENDING_NUM (PENDING_WINDOW_SURFACE_COMMIT + 1)

#endif /* USE_EVENT_BACKGROUND_THREAD */

struct GWL_WindowFrame {
  /**
   * The frame size (in GHOST window coordinates).
   *
   * These must be converted to WAYLADN relative coordinates when the window is scaled
   * by Hi-DPI/fractional scaling.
   */
  int32_t size[2] = {0, 0};
  bool is_maximised = false;
  bool is_fullscreen = false;
  bool is_active = false;
  /** Disable when the fractional scale is a whole number. */
  int fractional_scale = 0;
  /**
   * Store the value of #wp_fractional_scale_v1_listener::preferred_scale
   * before it's applied.
   */
  int fractional_scale_preferred = 0;
  /** The scale passed to #wl_surface_set_buffer_scale. */
  int buffer_scale = 0;

  /** Scale has been set (for the first time). */
  bool is_scale_init = false;
};

struct GWL_Window {

  /** Wayland core types. */
  struct {
    wl_surface *surface = nullptr;
  } wl;

  /** Wayland native types. */
  struct {
    wp_viewport *viewport = nullptr;

    /**
     * When set, only respond to the #wp_fractional_scale_v1_listener::preferred_scale callback
     * and ignore updated scale based on #wl_surface_listener::enter & exit events.
     */
    wp_fractional_scale_v1 *fractional_scale_handle = nullptr;
  } wp;

  /** XDG native types. */
  struct {
    /** A temporary token used for the window to be notified of it's activation. */
    xdg_activation_token_v1 *activation_token = nullptr;
  } xdg;

  struct {
#ifdef WITH_OPENGL_BACKEND
    wl_egl_window *egl_window = nullptr;
#endif
#ifdef WITH_VULKAN_BACKEND
    GHOST_ContextVK_WindowInfo *vulkan_window_info = nullptr;
#endif
  } backend;

  GHOST_WindowWayland *ghost_window = nullptr;
  GHOST_SystemWayland *ghost_system = nullptr;
  GHOST_TDrawingContextType ghost_context_type = GHOST_kDrawingContextTypeNone;

  /**
   * Outputs on which the window is currently shown on.
   *
   * This is an ordered set (whoever adds to this is responsible for keeping members unique).
   * In practice this is rarely manipulated and is limited by the number of physical displays.
   */
  std::vector<GWL_Output *> outputs;

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  GWL_LibDecor_Window *libdecor = nullptr;
#endif
  GWL_XDG_Decor_Window *xdg_decor = nullptr;

  /**
   * The current value of frame, copied from `frame_pending` when applying updates.
   * This avoids the need for locking when reading from `frame`.
   */
  GWL_WindowFrame frame;
  GWL_WindowFrame frame_pending;

#ifdef USE_EVENT_BACKGROUND_THREAD
  /**
   * Needed so calls such as #GHOST_Window::setClientSize
   * doesn't conflict with WAYLAND callbacks that may run in a thread.
   */
  std::mutex frame_pending_mutex;
#endif

  std::string title;

  bool is_dialog = false;

  /** Currently only initialized on access (avoids allocations & allows to keep private). */
  GWL_WindowScaleParams scale_params;

#ifdef USE_EVENT_BACKGROUND_THREAD
  /**
   * These pending actions can't be performed when WAYLAND handlers are running from a thread.
   * Postpone their execution until the main thread can handle them.
   */
  std::atomic<bool> pending_actions[PENDING_NUM] = {false};
#endif /* USE_EVENT_BACKGROUND_THREAD */
};

static void gwl_window_resize_for_backend(GWL_Window *win, const int32_t size[2])
{
#ifdef WITH_OPENGL_BACKEND
  if (win->ghost_context_type == GHOST_kDrawingContextTypeOpenGL) {
    /* Null on window initialization. */
    if (win->backend.egl_window) {
      wl_egl_window_resize(win->backend.egl_window, UNPACK2(size), 0, 0);
    }
  }
#endif
#ifdef WITH_VULKAN_BACKEND
  if (win->ghost_context_type == GHOST_kDrawingContextTypeVulkan) {
    win->backend.vulkan_window_info->size[0] = size[0];
    win->backend.vulkan_window_info->size[1] = size[1];
  }
#endif
}

static void gwl_window_title_set(GWL_Window *win, const char *title)
{
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    GWL_LibDecor_Window &decor = *win->libdecor;
    libdecor_frame_set_title(decor.frame, title);
  }
  else
#endif
  {
    GWL_XDG_Decor_Window &decor = *win->xdg_decor;
    xdg_toplevel_set_title(decor.toplevel, title);
  }

  win->title = title;
}

static GHOST_TWindowState gwl_window_state_get(const GWL_Window *win)
{
  if (win->frame.is_fullscreen) {
    return GHOST_kWindowStateFullScreen;
  }
  if (win->frame.is_maximised) {
    return GHOST_kWindowStateMaximized;
  }
  return GHOST_kWindowStateNormal;
}

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/**
 * \note Keep in sync with #gwl_window_state_set_for_xdg.
 */
static bool gwl_window_state_set_for_libdecor(libdecor_frame *frame,
                                              const GHOST_TWindowState state,
                                              const GHOST_TWindowState state_current)
{
  switch (state) {
    case GHOST_kWindowStateNormal:
      /* Unset states. */
      switch (state_current) {
        case GHOST_kWindowStateMaximized: {
          libdecor_frame_unset_maximized(frame);
          break;
        }
        case GHOST_kWindowStateFullScreen: {
          libdecor_frame_unset_fullscreen(frame);
          break;
        }
        default: {
          break;
        }
      }
      break;
    case GHOST_kWindowStateMaximized: {
      libdecor_frame_set_maximized(frame);
      break;
    }
    case GHOST_kWindowStateMinimized: {
      libdecor_frame_set_minimized(frame);
      break;
    }
    case GHOST_kWindowStateFullScreen: {
      libdecor_frame_set_fullscreen(frame, nullptr);
      break;
    }
  }
  return true;
}

#endif /* WITH_GHOST_WAYLAND_LIBDECOR */

/**
 * \note Keep in sync with #gwl_window_state_set_for_libdecor.
 */
static bool gwl_window_state_set_for_xdg(xdg_toplevel *toplevel,
                                         const GHOST_TWindowState state,
                                         const GHOST_TWindowState state_current)
{
  switch (state) {
    case GHOST_kWindowStateNormal:
      /* Unset states. */
      switch (state_current) {
        case GHOST_kWindowStateMaximized: {
          xdg_toplevel_unset_maximized(toplevel);
          break;
        }
        case GHOST_kWindowStateFullScreen: {
          xdg_toplevel_unset_fullscreen(toplevel);
          break;
        }
        default: {
          break;
        }
      }
      break;
    case GHOST_kWindowStateMaximized: {
      xdg_toplevel_set_maximized(toplevel);
      break;
    }
    case GHOST_kWindowStateMinimized: {
      xdg_toplevel_set_minimized(toplevel);
      break;
    }
    case GHOST_kWindowStateFullScreen: {
      xdg_toplevel_set_fullscreen(toplevel, nullptr);
      break;
    }
  }
  return true;
}

static bool gwl_window_state_set(GWL_Window *win, const GHOST_TWindowState state)
{
  const GHOST_TWindowState state_current = gwl_window_state_get(win);
  bool result;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    result = gwl_window_state_set_for_libdecor(win->libdecor->frame, state, state_current);
  }
  else
#endif
  {
    result = gwl_window_state_set_for_xdg(win->xdg_decor->toplevel, state, state_current);
  }
  return result;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Viewport Setup/Tear-down
 *
 * A viewport is needed to implement fractional scale,
 * as the outputs scale may change at runtime, support creating & clearing the viewport as needed.
 * \{ */

/**
 * Scale a value from a viewport value to Wayland windowing.
 * Scale down or not at all.
 */
static int gwl_window_fractional_to_viewport(const GWL_WindowFrame &frame, int value)
{
  GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
  return (value * frame.fractional_scale) / FRACTIONAL_DENOMINATOR;
}

/**
 * Scale a value from a Wayland windowing value to the viewport.
 * Scales up or not at all.
 */
static int gwl_window_fractional_from_viewport(const GWL_WindowFrame &frame, int value)
{
  GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
  return (value * FRACTIONAL_DENOMINATOR) / frame.fractional_scale;
}

/* NOTE: rounded versions are needed for window-frame dimensions conversions.
 * (rounding is part of the WAYLAND spec). All other conversions such as cursor coordinates
 * can used simple integer division as rounding is not defined in this case. */

static int gwl_window_fractional_to_viewport_round(const GWL_WindowFrame &frame, int value)
{
  GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
  return lroundf(double(value * frame.fractional_scale) / double(FRACTIONAL_DENOMINATOR));
}

static int gwl_window_fractional_from_viewport_round(const GWL_WindowFrame &frame, int value)
{
  GHOST_ASSERT(frame.fractional_scale != 0, "Not fractional or called before initialized!");
  return lroundf(double(value * FRACTIONAL_DENOMINATOR) / double(frame.fractional_scale));
}

static bool gwl_window_viewport_set(GWL_Window *win,
                                    bool *r_surface_needs_commit,
                                    bool *r_surface_needs_buffer_scale)
{
  if (win->wp.viewport != nullptr) {
    return false;
  }
  wp_viewporter *viewporter = win->ghost_system->wp_viewporter_get();
  if (viewporter == nullptr) {
    return false;
  }
  win->wp.viewport = wp_viewporter_get_viewport(viewporter, win->wl.surface);
  if (win->wp.viewport == nullptr) {
    return false;
  }

  /* Set the buffer scale to 1 since a viewport will be used. */
  if (win->frame.buffer_scale != 1) {
    win->frame.buffer_scale = 1;

    if (r_surface_needs_buffer_scale) {
      *r_surface_needs_buffer_scale = true;
    }
    else {
      wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
    }

    if (r_surface_needs_commit) {
      *r_surface_needs_commit = true;
    }
    else {
      wl_surface_commit(win->wl.surface);
    }
  }

  return true;
}

static bool gwl_window_viewport_unset(GWL_Window *win,
                                      bool *r_surface_needs_commit,
                                      bool *r_surface_needs_buffer_scale)
{
  if (win->wp.viewport == nullptr) {
    return false;
  }

  wp_viewport_destroy(win->wp.viewport);
  win->wp.viewport = nullptr;

  GHOST_ASSERT(win->frame.buffer_scale == 1, "Unexpected scale!");
  if (win->frame_pending.buffer_scale != win->frame.buffer_scale) {
    win->frame.buffer_scale = win->frame_pending.buffer_scale;

    if (r_surface_needs_buffer_scale) {
      *r_surface_needs_buffer_scale = true;
    }
    else {
      wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
    }

    if (r_surface_needs_commit) {
      *r_surface_needs_commit = true;
    }
    else {
      wl_surface_commit(win->wl.surface);
    }
  }
  return true;
}

static bool gwl_window_viewport_size_update(GWL_Window *win)
{
  if (win->wp.viewport == nullptr) {
    return false;
  }
  wp_viewport_set_source(win->wp.viewport,
                         wl_fixed_from_int(0),
                         wl_fixed_from_int(0),
                         wl_fixed_from_int(win->frame.size[0]),
                         wl_fixed_from_int(win->frame.size[1]));
  wp_viewport_set_destination(
      win->wp.viewport,
      gwl_window_fractional_from_viewport_round(win->frame, win->frame.size[0]),
      gwl_window_fractional_from_viewport_round(win->frame, win->frame.size[1]));
  return true;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Activation
 * \{ */

/**
 * Unlike #GHOST_WindowWayland::activate which responds to WAYLAND having set the window active.
 * This function makes a request to WAYLAND for the window to become active.
 *
 * \note The request may be ignored and may not work on all compositors.
 */
static void gwl_window_activate(GWL_Window *win)
{
  GHOST_SystemWayland *system = win->ghost_system;
  xdg_activation_v1 *activation_manager = system->xdg_activation_manager_get();
  if (UNLIKELY(activation_manager == nullptr)) {
    return;
  }

  if (win->xdg.activation_token) {
    /* We're about to overwrite this with a new request. */
    xdg_activation_token_v1_destroy(win->xdg.activation_token);
  }
  win->xdg.activation_token = xdg_activation_v1_get_activation_token(activation_manager);

  xdg_activation_token_v1_add_listener(
      win->xdg.activation_token, xdg_activation_listener_get(), win);

  xdg_activation_token_v1_set_app_id(win->xdg.activation_token,
                                     GHOST_SystemWayland::xdg_app_id_get());

  /* The serial of the input device requesting activation. */
  {
    uint32_t serial = 0;
    wl_seat *seat = system->wl_seat_active_get_with_input_serial(serial);
    if (seat) {
      xdg_activation_token_v1_set_serial(win->xdg.activation_token, serial, seat);
    }
  }

  /* The surface of the window requesting activation. */
  {
    GHOST_WindowWayland *ghost_window_active = static_cast<GHOST_WindowWayland *>(
        system->getWindowManager()->getActiveWindow());
    if (ghost_window_active) {
      wl_surface *surface = ghost_window_active->wl_surface_get();
      if (surface) {
        xdg_activation_token_v1_set_surface(win->xdg.activation_token, surface);
      }
    }
  }

  xdg_activation_token_v1_commit(win->xdg.activation_token);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Pending Actions
 * \{ */

static void gwl_window_frame_pending_fractional_scale_set_notest(
    GWL_Window *win, bool *r_surface_needs_commit, bool *r_surface_needs_buffer_scale)
{
  if (win->frame_pending.fractional_scale) {
    win->frame.fractional_scale = win->frame_pending.fractional_scale;
    gwl_window_viewport_set(win, r_surface_needs_commit, r_surface_needs_buffer_scale);
    gwl_window_viewport_size_update(win);
  }
  else {
    if (win->wp.viewport) {
      gwl_window_viewport_unset(win, r_surface_needs_commit, r_surface_needs_buffer_scale);
    }
    else {
      win->frame.buffer_scale = win->frame_pending.buffer_scale;
      if (r_surface_needs_buffer_scale) {
        *r_surface_needs_buffer_scale = true;
      }
      else {
        wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
      }
      if (r_surface_needs_commit) {
        *r_surface_needs_commit = true;
      }
      else {
        wl_surface_commit(win->wl.surface);
      }
    }
  }
}

static void gwl_window_frame_pending_fractional_scale_set(GWL_Window *win,
                                                          bool *r_surface_needs_commit,
                                                          bool *r_surface_needs_buffer_scale)
{
  if (win->frame_pending.fractional_scale == win->frame.fractional_scale &&
      win->frame_pending.buffer_scale == win->frame.buffer_scale)
  {
    return;
  }
  gwl_window_frame_pending_fractional_scale_set_notest(
      win, r_surface_needs_commit, r_surface_needs_buffer_scale);
}

static void gwl_window_frame_pending_size_set(GWL_Window *win,
                                              bool *r_surface_needs_commit,
                                              bool *r_surface_needs_resize_for_backend,
                                              bool *r_surface_needs_buffer_scale)
{
  if (win->frame_pending.size[0] == 0 || win->frame_pending.size[1] == 0) {
    return;
  }

  win->frame.size[0] = win->frame_pending.size[0];
  win->frame.size[1] = win->frame_pending.size[1];

  if (win->frame_pending.fractional_scale != win->frame.fractional_scale ||
      win->frame_pending.buffer_scale != win->frame.buffer_scale)
  {
    gwl_window_frame_pending_fractional_scale_set(
        win, r_surface_needs_commit, r_surface_needs_buffer_scale);
  }
  else {
    gwl_window_viewport_size_update(win);
  }

  if (r_surface_needs_resize_for_backend) {
    *r_surface_needs_resize_for_backend = true;
  }
  else {
    gwl_window_resize_for_backend(win, win->frame.size);
  }

  win->ghost_window->notify_size();

  win->frame_pending.size[0] = 0;
  win->frame_pending.size[1] = 0;
}

static void gwl_window_frame_update_from_pending(GWL_Window *win);

#ifdef USE_EVENT_BACKGROUND_THREAD

static void gwl_window_pending_actions_tag(GWL_Window *win, enum eGWL_PendingWindowActions type)
{
  win->pending_actions[int(type)].store(true);
  win->ghost_system->has_pending_actions_for_window.store(true);
}

static void gwl_window_pending_actions_handle(GWL_Window *win)
{
  /* Ensure pending actions always use the state when the function starts
   * because one actions may trigger other pending actions an in that case
   * exact behavior depends on the order functions are called here.
   * Without this, configuring the frame will trigger the surface
   * commit immediately instead of the next time pending actions are handled. */
  bool actions[PENDING_NUM];
  for (size_t i = 0; i < ARRAY_SIZE(actions); i++) {
    actions[i] = win->pending_actions[i].exchange(false);
  }

  if (actions[PENDING_WINDOW_FRAME_CONFIGURE]) {
    gwl_window_frame_update_from_pending(win);
  }
  if (actions[PENDING_OUTPUT_SCALE_UPDATE_DEFERRED]) {
    gwl_window_pending_actions_tag(win, PENDING_OUTPUT_SCALE_UPDATE);
    /* Force postponing scale update to ensure all scale information has been taken into account
     * before the actual update is performed. Failing to do so tends to cause flickering. */
    actions[PENDING_OUTPUT_SCALE_UPDATE] = false;
  }
  if (actions[PENDING_OUTPUT_SCALE_UPDATE]) {
    win->ghost_window->outputs_changed_update_scale();
  }
  if (actions[PENDING_WINDOW_SURFACE_COMMIT]) {
    wl_surface_commit(win->wl.surface);
  }
}

#endif /* USE_EVENT_BACKGROUND_THREAD */

/**
 * Update the window's #GWL_WindowFrame.
 * The caller must handle locking & run from the main thread.
 */
static void gwl_window_frame_update_from_pending_no_lock(GWL_Window *win)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  GHOST_ASSERT(win->ghost_system->main_thread_id == std::this_thread::get_id(),
               "Only from main thread!");

#endif

  const bool dpi_changed = win->frame_pending.fractional_scale != win->frame.fractional_scale;
  bool surface_needs_commit = false;
  bool surface_needs_resize_for_backend = false;
  bool surface_needs_buffer_scale = false;

  if (win->frame_pending.size[0] != 0 && win->frame_pending.size[1] != 0) {
    if ((win->frame.size[0] != win->frame_pending.size[0]) ||
        (win->frame.size[1] != win->frame_pending.size[1]))
    {
      gwl_window_frame_pending_size_set(win,
                                        &surface_needs_commit,
                                        &surface_needs_resize_for_backend,
                                        &surface_needs_buffer_scale);
    }
  }

  if (win->frame_pending.fractional_scale || win->frame.fractional_scale) {
    gwl_window_frame_pending_fractional_scale_set(
        win, &surface_needs_commit, &surface_needs_buffer_scale);
  }
  else {
    if (win->frame_pending.buffer_scale != win->frame.buffer_scale) {
      win->frame.buffer_scale = win->frame_pending.buffer_scale;
      surface_needs_buffer_scale = true;
    }
  }

  if (surface_needs_resize_for_backend) {
    gwl_window_resize_for_backend(win, win->frame.size);
  }

  if (surface_needs_buffer_scale) {
    wl_surface_set_buffer_scale(win->wl.surface, win->frame.buffer_scale);
  }

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    GWL_LibDecor_Window &decor = *win->libdecor;
    if (decor.pending.ack_configure) {
      surface_needs_commit = true;

      decor.pending.ack_configure = false;

      libdecor_configuration *configuration = decor.pending.configuration;

      libdecor_state *state = libdecor_state_new(UNPACK2(decor.pending.size));
      libdecor_frame_commit(decor.frame, state, configuration);
      libdecor_state_free(state);

      decor.pending.configuration = nullptr;
      decor.pending.size[0] = 0;
      decor.pending.size[1] = 0;

      decor.initial_configure_seen = true;

#  ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
      if (decor.pending.configuration_needs_free) {
        ghost_wl_libdecor_configuration_free(decor.pending.configuration);
        decor.pending.configuration_needs_free = false;
      }
#  endif
    }
  }
  else
#endif
      if (win->xdg_decor)
  {
    GWL_XDG_Decor_Window &decor = *win->xdg_decor;
    if (decor.pending.ack_configure) {
      xdg_surface_ack_configure(decor.surface, decor.pending.ack_configure_serial);
      /* The XDG spec states a commit event is required after ACK configure. */
      surface_needs_commit = true;

      decor.pending.ack_configure = false;
      decor.pending.ack_configure_serial = 0;

      decor.initial_configure_seen = true;
    }
  }

  if (surface_needs_commit) {
#ifdef USE_EVENT_BACKGROUND_THREAD
    /* Postponing the commit avoids flickering when moving between monitors of different scale. */
    gwl_window_pending_actions_tag(win, PENDING_WINDOW_SURFACE_COMMIT);
#else
    wl_surface_commit(win->wl.surface);
#endif
  }

  if (dpi_changed) {
    GHOST_SystemWayland *system = win->ghost_system;
    system->pushEvent(new GHOST_Event(
        system->getMilliSeconds(), GHOST_kEventWindowDPIHintChanged, win->ghost_window));
  }

  if (win->frame.is_active != win->frame_pending.is_active) {
    if (win->frame_pending.is_active) {
      win->ghost_window->activate();
    }
    else {
      win->ghost_window->deactivate();
    }
  }
  else if (false) {
    /* Disabled, this can happen during debugging
     * when the window changed while the process has been paused. */
    GHOST_ASSERT(
        win->frame.is_active ==
            (win->ghost_system->getWindowManager()->getActiveWindow() == win->ghost_window),
        "GHOST internal active state does not match WAYLAND!");
  }

  win->frame_pending.size[0] = win->frame.size[0];
  win->frame_pending.size[1] = win->frame.size[1];

  win->frame = win->frame_pending;

  /* Signal not to apply the scale unless it's configured. */
  win->frame_pending.size[0] = 0;
  win->frame_pending.size[1] = 0;
}

[[maybe_unused]] static void gwl_window_frame_update_from_pending(GWL_Window *win)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{win->frame_pending_mutex};
#endif
  gwl_window_frame_update_from_pending_no_lock(win);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal Utilities
 * \{ */

/**
 * Return -1 if `output_a` has a scale smaller than `output_b`, 0 when there equal, otherwise 1.
 */
static int output_scale_cmp(const GWL_Output *output_a, const GWL_Output *output_b)
{
  if (output_a->scale < output_b->scale) {
    return -1;
  }
  if (output_a->scale > output_b->scale) {
    return 1;
  }
  if (output_a->has_scale_fractional || output_b->has_scale_fractional) {
    const int scale_fractional_a = output_a->has_scale_fractional ?
                                       output_a->scale_fractional :
                                       (output_a->scale * FRACTIONAL_DENOMINATOR);
    const int scale_fractional_b = output_b->has_scale_fractional ?
                                       output_b->scale_fractional :
                                       (output_b->scale * FRACTIONAL_DENOMINATOR);
    if (scale_fractional_a < scale_fractional_b) {
      return -1;
    }
    if (scale_fractional_a > scale_fractional_b) {
      return 1;
    }
  }
  return 0;
}

static int outputs_max_scale_or_default(const std::vector<GWL_Output *> &outputs,
                                        const int32_t scale_default,
                                        int *r_scale_fractional)
{
  const GWL_Output *output_max = nullptr;
  for (const GWL_Output *reg_output : outputs) {
    if (!output_max || (output_scale_cmp(output_max, reg_output) == -1)) {
      output_max = reg_output;
    }
  }

  if (output_max) {
    if (r_scale_fractional) {
      *r_scale_fractional = output_max->has_scale_fractional ?
                                output_max->scale_fractional :
                                (output_max->scale * FRACTIONAL_DENOMINATOR);
    }
    return output_max->scale;
  }
  if (r_scale_fractional) {
    *r_scale_fractional = scale_default * FRACTIONAL_DENOMINATOR;
  }
  return scale_default;
}

static int outputs_uniform_scale_or_default(const std::vector<GWL_Output *> &outputs,
                                            const int32_t scale_default,
                                            int *r_scale_fractional)
{
  const GWL_Output *output_uniform = nullptr;
  for (const GWL_Output *reg_output : outputs) {
    if (!output_uniform) {
      output_uniform = reg_output;
    }
    else if (output_scale_cmp(output_uniform, reg_output) != 0) {
      /* Non-uniform. */
      output_uniform = nullptr;
      break;
    }
  }

  if (output_uniform) {
    if (r_scale_fractional) {
      *r_scale_fractional = output_uniform->has_scale_fractional ?
                                output_uniform->scale_fractional :
                                (output_uniform->scale * FRACTIONAL_DENOMINATOR);
    }
    return output_uniform->scale;
  }
  if (r_scale_fractional) {
    *r_scale_fractional = scale_default * FRACTIONAL_DENOMINATOR;
  }
  return scale_default;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name LIBDECOR Configuration Workaround
 *
 * These workarounds are needed until LIBDECOR supports copy/free.
 * \{ */

#ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND

static libdecor_configuration *ghost_wl_libdecor_configuration_copy(
    const libdecor_configuration *configuration)
{
  size_t configuration_size = malloc_usable_size((void *)configuration);
  libdecor_configuration *configuration_copy = (libdecor_configuration *)malloc(
      configuration_size);
  memcpy((void *)configuration_copy, (const void *)configuration, configuration_size);
  return configuration_copy;
}

static void ghost_wl_libdecor_configuration_free(libdecor_configuration *configuration)
{
  free((void *)configuration);
}

#endif /* USE_LIBDECOR_CONFIG_COPY_WORKAROUND */

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Top Level), #xdg_toplevel_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_TOPLEVEL = {"ghost.wl.handle.xdg_toplevel"};
#define LOG (&LOG_WL_XDG_TOPLEVEL)

static void xdg_toplevel_handle_configure(void *data,
                                          xdg_toplevel * /*xdg_toplevel*/,
                                          const int32_t width,
                                          const int32_t height,
                                          wl_array *states)
{
  /* TODO: log `states`, not urgent. */
  CLOG_INFO(LOG, 2, "configure (size=[%d, %d])", width, height);

  GWL_Window *win = static_cast<GWL_Window *>(data);

#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{win->frame_pending_mutex};
#endif

  const int32_t size[2] = {width, height};
  for (int i = 0; i < 2; i++) {
    if (size[i] == 0) {
      /* Values may be zero, in this case the client should choose. */
      continue;
    }
    win->frame_pending.size[i] = win->frame.fractional_scale ?
                                     gwl_window_fractional_to_viewport_round(win->frame, size[i]) :
                                     (size[i] * win->frame.buffer_scale);
  }

  win->frame_pending.is_maximised = false;
  win->frame_pending.is_fullscreen = false;
  win->frame_pending.is_active = false;

  enum xdg_toplevel_state *state;
  WL_ARRAY_FOR_EACH (state, states) {
    switch (*state) {
      case XDG_TOPLEVEL_STATE_MAXIMIZED:
        win->frame_pending.is_maximised = true;
        break;
      case XDG_TOPLEVEL_STATE_FULLSCREEN:
        win->frame_pending.is_fullscreen = true;
        break;
      case XDG_TOPLEVEL_STATE_ACTIVATED:
        win->frame_pending.is_active = true;
        break;
      default:
        break;
    }
  }
}

static void xdg_toplevel_handle_close(void *data, xdg_toplevel * /*xdg_toplevel*/)
{
  CLOG_INFO(LOG, 2, "close");

  GWL_Window *win = static_cast<GWL_Window *>(data);

  win->ghost_window->close();
}

static const xdg_toplevel_listener xdg_toplevel_listener = {
    /*configure*/ xdg_toplevel_handle_configure,
    /*close*/ xdg_toplevel_handle_close,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Activation), #xdg_activation_v1_interface
 *
 * Used by #gwl_window_activate.
 * \{ */

static void xdg_activation_handle_done(void *data,
                                       xdg_activation_token_v1 *xdg_activation_token_v1,
                                       const char *token)
{
  GWL_Window *win = static_cast<GWL_Window *>(data);
  if (xdg_activation_token_v1 != win->xdg.activation_token) {
    return;
  }

  GHOST_SystemWayland *system = win->ghost_system;
  xdg_activation_v1 *activation_manager = system->xdg_activation_manager_get();
  xdg_activation_v1_activate(activation_manager, token, win->wl.surface);
  xdg_activation_token_v1_destroy(win->xdg.activation_token);
  win->xdg.activation_token = nullptr;
}

static const xdg_activation_token_v1_listener xdg_activation_listener = {
    /*done*/ xdg_activation_handle_done,
};

static const xdg_activation_token_v1_listener *xdg_activation_listener_get()
{
  return &xdg_activation_listener;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (Fractional Scale), #wp_fractional_scale_manager_v1_interface
 *
 * Used by #gwl_window_activate.
 * \{ */

static CLG_LogRef LOG_WL_FRACTIONAL_SCALE = {"ghost.wl.handle.fractional_scale"};
#define LOG (&LOG_WL_FRACTIONAL_SCALE)

static void wp_fractional_scale_handle_preferred_scale(
    void *data, wp_fractional_scale_v1 * /*wp_fractional_scale_v1*/, uint preferred_scale)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
#endif
  CLOG_INFO(LOG,
            2,
            "preferred_scale (preferred_scale=%.6f)",
            double(preferred_scale) / FRACTIONAL_DENOMINATOR);

  GWL_Window *win = static_cast<GWL_Window *>(data);

  if (win->frame_pending.fractional_scale_preferred != int(preferred_scale)) {
    win->frame_pending.fractional_scale_preferred = preferred_scale;
    win->ghost_window->outputs_changed_update_scale_tag();
  }
}

static const wp_fractional_scale_v1_listener wp_fractional_scale_listener = {
    /*preferred_scale*/ wp_fractional_scale_handle_preferred_scale,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (LibDecor Frame), #libdecor_frame_interface
 * \{ */

#ifdef WITH_GHOST_WAYLAND_LIBDECOR

static CLG_LogRef LOG_WL_LIBDECOR_FRAME = {"ghost.wl.handle.libdecor_frame"};
#  define LOG (&LOG_WL_LIBDECOR_FRAME)

static void libdecor_frame_handle_configure(libdecor_frame *frame,
                                            libdecor_configuration *configuration,
                                            void *data)
{
  CLOG_INFO(LOG, 2, "configure");

#  ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
  const bool is_main_thread = [data] {
    const GWL_Window *win = static_cast<GWL_Window *>(data);
    const GHOST_SystemWayland *system = win->ghost_system;
    return system->main_thread_id == std::this_thread::get_id();
  }();
#  endif

  GWL_WindowFrame &frame_pending = static_cast<GWL_Window *>(data)->frame_pending;

  /* Set the size. */
  int size_next[2] = {0, 0};

  {
    GWL_Window *win = static_cast<GWL_Window *>(data);
    const int fractional_scale = win->frame.fractional_scale;
    /* It's important `fractional_scale` has a fractional component or rounding up will fail
     * to produce the correct whole-number scale. */
    GHOST_ASSERT((fractional_scale == 0) ||
                     (gwl_round_int_test(fractional_scale, FRACTIONAL_DENOMINATOR) == false),
                 "Fractional scale has no fractional component!");
    /* The size from LIBDECOR wont use the GHOST windows buffer size.
     * so it's important to calculate the buffer size that would have been used
     * if fractional scaling wasn't supported. */
    const int scale = fractional_scale ? (fractional_scale / FRACTIONAL_DENOMINATOR) + 1 :
                                         win->frame.buffer_scale;
    const int scale_as_fractional = scale * FRACTIONAL_DENOMINATOR;
    if (libdecor_configuration_get_content_size(
            configuration, frame, &size_next[0], &size_next[1]))
    {
      if (fractional_scale) {
        frame_pending.size[0] = gwl_window_fractional_to_viewport_round(win->frame, size_next[0]);
        frame_pending.size[1] = gwl_window_fractional_to_viewport_round(win->frame, size_next[1]);
      }
      else if (fractional_scale && (fractional_scale != (scale * FRACTIONAL_DENOMINATOR))) {
        /* The windows `preferred_scale` is not yet available,
         * set the size as if fractional scale is available. */
        frame_pending.size[0] = ((size_next[0] * scale) * fractional_scale) / scale_as_fractional;
        frame_pending.size[1] = ((size_next[1] * scale) * fractional_scale) / scale_as_fractional;
      }
      else {
        frame_pending.size[0] = size_next[0] * scale;
        frame_pending.size[1] = size_next[1] * scale;
      }

      /* Account for buffer rounding requirement, once fractional scaling is enabled
       * the buffer scale will be 1, rounding is a requirement until then. */
      gwl_round_int2_by(frame_pending.size, win->frame.buffer_scale);
    }
  }

  /* Set the state. */
  {
    enum libdecor_window_state window_state;
    if (libdecor_configuration_get_window_state(configuration, &window_state)) {
      frame_pending.is_maximised = window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED;
      frame_pending.is_fullscreen = window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN;
      frame_pending.is_active = window_state & LIBDECOR_WINDOW_STATE_ACTIVE;
    }
  }

  if (size_next[0] && size_next[1]) {
    GWL_Window *win = static_cast<GWL_Window *>(data);
    GWL_LibDecor_Window &decor = *win->libdecor;

#  ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
    /* Unlikely but possible a previous configuration is unhandled. */
    if (decor.pending.configuration_needs_free) {
      ghost_wl_libdecor_configuration_free(decor.pending.configuration);
      decor.pending.configuration_needs_free = false;
    }
#  endif /* USE_LIBDECOR_CONFIG_COPY_WORKAROUND */

    decor.pending.size[0] = size_next[0];
    decor.pending.size[1] = size_next[1];
    decor.pending.configuration = configuration;
    decor.pending.ack_configure = true;

    if (!is_main_thread) {
#  ifdef USE_LIBDECOR_CONFIG_COPY_WORKAROUND
      decor.pending.configuration = ghost_wl_libdecor_configuration_copy(configuration);
      decor.pending.configuration_needs_free = true;
#  else
      /* Without a way to copy the configuration,
       * the configuration will be ignored as it can't be postponed. */
      decor.pending.configuration = nullptr;
#  endif /* !USE_LIBDECOR_CONFIG_COPY_WORKAROUND */
    }
  }

  /* Apply & commit the changes. */
  {
    GWL_Window *win = static_cast<GWL_Window *>(data);
#  ifdef USE_EVENT_BACKGROUND_THREAD
    if (!is_main_thread) {
      gwl_window_pending_actions_tag(win, PENDING_WINDOW_FRAME_CONFIGURE);
    }
    else
#  endif
    {
      gwl_window_frame_update_from_pending_no_lock(win);
    }
  }
}

static void libdecor_frame_handle_close(libdecor_frame * /*frame*/, void *data)
{
  CLOG_INFO(LOG, 2, "close");

  GWL_Window *win = static_cast<GWL_Window *>(data);

  win->ghost_window->close();
}

static void libdecor_frame_handle_commit(libdecor_frame * /*frame*/, void *data)
{
  CLOG_INFO(LOG, 2, "commit");

#  if 0
  GWL_Window *win = static_cast<GWL_Window *>(data);
  win->ghost_window->notify_decor_redraw();
#  else
  (void)data;
#  endif
}

/* NOTE: cannot be `const` because of the LIBDECOR API. */
static libdecor_frame_interface libdecor_frame_iface = {
    /*configure*/ libdecor_frame_handle_configure,
    /*close*/ libdecor_frame_handle_close,
    /*commit*/ libdecor_frame_handle_commit,
};

#  undef LOG

#endif /* WITH_GHOST_WAYLAND_LIBDECOR. */

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Decoration Listener), #zxdg_toplevel_decoration_v1_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_TOPLEVEL_DECORATION = {"ghost.wl.handle.xdg_toplevel_decoration"};
#define LOG (&LOG_WL_XDG_TOPLEVEL_DECORATION)

static void xdg_toplevel_decoration_handle_configure(
    void *data, zxdg_toplevel_decoration_v1 * /*zxdg_toplevel_decoration_v1*/, const uint32_t mode)
{
  CLOG_INFO(LOG, 2, "configure (mode=%u)", mode);

  GWL_Window *win = static_cast<GWL_Window *>(data);

  win->xdg_decor->mode = (zxdg_toplevel_decoration_v1_mode)mode;
}

static const zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_v1_listener = {
    /*configure*/ xdg_toplevel_decoration_handle_configure,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Surface Handle Configure), #xdg_surface_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_SURFACE = {"ghost.wl.handle.xdg_surface"};
#define LOG (&LOG_WL_XDG_SURFACE)

static void xdg_surface_handle_configure(void *data,
                                         xdg_surface *xdg_surface,
                                         const uint32_t serial)
{
  GWL_Window *win = static_cast<GWL_Window *>(data);

  if (win->xdg_decor->surface != xdg_surface) {
    CLOG_INFO(LOG, 2, "configure (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "configure");

#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
#endif
  win->xdg_decor->pending.ack_configure = true;
  win->xdg_decor->pending.ack_configure_serial = serial;

#ifdef USE_EVENT_BACKGROUND_THREAD
  const GHOST_SystemWayland *system = win->ghost_system;
  const bool is_main_thread = system->main_thread_id == std::this_thread::get_id();
  if (!is_main_thread) {
    /* NOTE(@ideasman42): this only gets one redraw,
     * I could not find a case where this causes problems. */
    gwl_window_pending_actions_tag(win, PENDING_WINDOW_FRAME_CONFIGURE);
  }
  else
#endif
  {
    gwl_window_frame_update_from_pending_no_lock(win);
  }
}

static const xdg_surface_listener xdg_surface_listener = {
    /*configure*/ xdg_surface_handle_configure,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (Surface), #wl_surface_listener
 * \{ */

static CLG_LogRef LOG_WL_SURFACE = {"ghost.wl.handle.surface"};
#define LOG (&LOG_WL_SURFACE)

static void surface_handle_enter(void *data, wl_surface * /*wl_surface*/, wl_output *wl_output)
{
  if (!ghost_wl_output_own(wl_output)) {
    CLOG_INFO(LOG, 2, "enter (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "enter");

  GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
  GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(data);
  if (win->outputs_enter(reg_output)) {
    win->outputs_changed_update_scale_tag();
  }
}

static void surface_handle_leave(void *data, wl_surface * /*wl_surface*/, wl_output *wl_output)
{
  if (!ghost_wl_output_own(wl_output)) {
    CLOG_INFO(LOG, 2, "leave (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "leave");

  GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
  GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(data);
  if (win->outputs_leave(reg_output)) {
    win->outputs_changed_update_scale_tag();
  }
}

static const wl_surface_listener wl_surface_listener = {
    /*enter*/ surface_handle_enter,
    /*leave*/ surface_handle_leave,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name GHOST Implementation
 *
 * WAYLAND specific implementation of the #GHOST_Window interface.
 * \{ */

GHOST_WindowWayland::GHOST_WindowWayland(GHOST_SystemWayland *system,
                                         const char *title,
                                         const int32_t /*left*/,
                                         const int32_t /*top*/,
                                         const uint32_t width,
                                         const uint32_t height,
                                         const GHOST_TWindowState state,
                                         const GHOST_IWindow *parentWindow,
                                         const GHOST_TDrawingContextType type,
                                         const bool is_dialog,
                                         const bool stereoVisual,
                                         const bool exclusive,
                                         const bool is_debug)
    : GHOST_Window(width, height, state, stereoVisual, exclusive),
      system_(system),
      window_(new GWL_Window),
      is_debug_context_(is_debug)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system->server_mutex};
#endif

  window_->ghost_window = this;
  window_->ghost_system = system;
  window_->ghost_context_type = type;

  /* NOTE(@ideasman42): The scale set here to avoid flickering on startup.
   * When all monitors use the same scale (which is quite common) there aren't any problems.
   *
   * When monitors have different scales there may still be a visible window resize on startup.
   * Ideally it would be possible to know the scale this window will use however that's only
   * known once #surface_enter callback runs (which isn't guaranteed to run at all).
   *
   * Using the maximum scale is best as it results in the window first being smaller,
   * avoiding a large window flashing before it's made smaller.
   *
   * For fractional scaling the buffer will eventually be 1. Setting it to 1 now
   * (to avoid window size rounding and buffer size switching) has some down-sides.
   * It means the window will be drawn larger for a moment then smaller once fractional scaling
   * is detected and enabled. Unfortunately, it doesn't seem possible to receive the
   * #wp_fractional_scale_v1_listener::preferred_scale information before the window is created
   * So leave the buffer scaled up because there is no *guarantee* the fractional scaling support
   * will run which could result in an incorrect buffer scale. */
  int scale_fractional_from_output;
  const int buffer_scale_from_output = outputs_uniform_scale_or_default(
      system_->outputs_get(), 0, &scale_fractional_from_output);

  window_->frame.size[0] = int32_t(width);
  window_->frame.size[1] = int32_t(height);

  window_->is_dialog = is_dialog;

  /* Window surfaces. */
  window_->wl.surface = wl_compositor_create_surface(system_->wl_compositor_get());
  ghost_wl_surface_tag(window_->wl.surface);

  wl_surface_add_listener(window_->wl.surface, &wl_surface_listener, window_);

  wp_fractional_scale_manager_v1 *fractional_scale_manager =
      system->wp_fractional_scale_manager_get();
  if (fractional_scale_manager) {
    window_->wp.fractional_scale_handle = wp_fractional_scale_manager_v1_get_fractional_scale(
        fractional_scale_manager, window_->wl.surface);
    wp_fractional_scale_v1_add_listener(
        window_->wp.fractional_scale_handle, &wp_fractional_scale_listener, window_);
  }

  /* NOTE: The limit is in points (not pixels) so Hi-DPI will limit to larger number of pixels.
   * This has the advantage that the size limit is the same when moving the window between monitors
   * with different scales set. If it was important to limit in pixels it could be re-calculated
   * when the `window_->frame.buffer_scale` changed. */
  const int32_t size_min[2] = {320, 240};

  const char *xdg_app_id = GHOST_SystemWayland::xdg_app_id_get();

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    window_->libdecor = new GWL_LibDecor_Window;
    GWL_LibDecor_Window &decor = *window_->libdecor;

    /* create window decorations */
    decor.frame = libdecor_decorate(
        system_->libdecor_context_get(), window_->wl.surface, &libdecor_frame_iface, window_);
    libdecor_frame_map(window_->libdecor->frame);

    libdecor_frame_set_min_content_size(decor.frame, UNPACK2(size_min));
    libdecor_frame_set_app_id(decor.frame, xdg_app_id);

    if (parentWindow) {
      GWL_LibDecor_Window &decor_parent =
          *dynamic_cast<const GHOST_WindowWayland *>(parentWindow)->window_->libdecor;
      libdecor_frame_set_parent(decor.frame, decor_parent.frame);
    }
  }
  else
#endif
  {
    window_->xdg_decor = new GWL_XDG_Decor_Window;
    GWL_XDG_Decor_Window &decor = *window_->xdg_decor;
    decor.surface = xdg_wm_base_get_xdg_surface(system_->xdg_decor_shell_get(),
                                                window_->wl.surface);
    decor.toplevel = xdg_surface_get_toplevel(decor.surface);

    xdg_toplevel_set_min_size(decor.toplevel, UNPACK2(size_min));
    xdg_toplevel_set_app_id(decor.toplevel, xdg_app_id);

    xdg_surface_add_listener(decor.surface, &xdg_surface_listener, window_);
    xdg_toplevel_add_listener(decor.toplevel, &xdg_toplevel_listener, window_);

    if (parentWindow && is_dialog) {
      GWL_XDG_Decor_Window &decor_parent =
          *dynamic_cast<const GHOST_WindowWayland *>(parentWindow)->window_->xdg_decor;
      xdg_toplevel_set_parent(decor.toplevel, decor_parent.toplevel);
    }
  }

  gwl_window_title_set(window_, title);

  wl_surface_set_user_data(window_->wl.surface, this);

  /* NOTE: the method used for XDG & LIBDECOR initialization (using `initial_configure_seen`)
   * follows the method used in SDL 3.16. */

  /* Causes a glitch with `libdecor` for some reason. */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    /* Pass. */
  }
  else
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
  {
    GWL_XDG_Decor_Window &decor = *window_->xdg_decor;

    if (system_->xdg_decor_manager_get()) {
      decor.toplevel_decor = zxdg_decoration_manager_v1_get_toplevel_decoration(
          system_->xdg_decor_manager_get(), decor.toplevel);
      zxdg_toplevel_decoration_v1_add_listener(
          decor.toplevel_decor, &xdg_toplevel_decoration_v1_listener, window_);
      zxdg_toplevel_decoration_v1_set_mode(decor.toplevel_decor,
                                           ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
    }

    /* Commit needed to so configure callback runs. */
    wl_surface_commit(window_->wl.surface);
    while (!decor.initial_configure_seen) {
      wl_display_flush(system->wl_display_get());
      wl_display_dispatch(system->wl_display_get());
    }
  }

  /* If the scale is known early, setup the window scale.
   * Otherwise accept an unsightly flicker once the outputs scale can be found. */
  int early_buffer_scale = 0;
  int early_fractional_scale = 0;

  if (const int test_fractional_scale =
          fractional_scale_manager ? (window_->frame_pending.fractional_scale_preferred ?
                                          window_->frame_pending.fractional_scale_preferred :
                                          scale_fractional_from_output) :
                                     0)
  {
    if (!gwl_round_int_test(test_fractional_scale, FRACTIONAL_DENOMINATOR)) {
      early_fractional_scale = test_fractional_scale;
    }
    else {
      /* Rounded, use simple integer buffer scaling. */
      early_buffer_scale = test_fractional_scale / FRACTIONAL_DENOMINATOR;
      early_fractional_scale = 0;
    }
  }
  else if (buffer_scale_from_output) {
    early_buffer_scale = buffer_scale_from_output;
  }

  if (early_fractional_scale != 0) {
    /* Fractional scale is known. */

    window_->frame.fractional_scale_preferred = early_fractional_scale;
    window_->frame.fractional_scale = early_fractional_scale;
    window_->frame.buffer_scale = 1;

    window_->frame_pending.fractional_scale_preferred = early_fractional_scale;
    window_->frame_pending.fractional_scale = early_fractional_scale;
    window_->frame_pending.buffer_scale = 1;

    /* The scale is considered initialized now. */
    window_->frame_pending.is_scale_init = true;

    /* Always commit and set the scale. */
    bool surface_needs_commit_dummy = false, surface_needs_buffer_scale_dummy = false;
    gwl_window_frame_pending_fractional_scale_set_notest(
        window_, &surface_needs_commit_dummy, &surface_needs_buffer_scale_dummy);
  }
  else if (early_buffer_scale != 0) {
    /* Non-fractional scale is known. */

    /* No fractional scale, simple initialization. */
    window_->frame.buffer_scale = early_buffer_scale;
    window_->frame_pending.buffer_scale = early_buffer_scale;

    /* The scale is considered initialized now. */
    window_->frame_pending.is_scale_init = true;

    /* The window surface must be rounded to the scale,
     * failing to do so causes the WAYLAND-server to close the window immediately. */
    gwl_round_int2_by(window_->frame.size, window_->frame.buffer_scale);
  }
  else {
    /* Scale isn't known (the windows size may flicker when #outputs_changed_update_scale runs). */
    window_->frame.buffer_scale = 1;
    window_->frame_pending.buffer_scale = 1;
    GHOST_ASSERT(window_->frame_pending.is_scale_init == false,
                 "An initialized scale is not expected");
  }

  if (window_->frame_pending.is_scale_init) {
    /* If the output scale changes here it means the scale settings were not properly set. */
    GHOST_ASSERT(outputs_changed_update_scale() == false,
                 "Fractional scale was not properly initialized");
  }

  wl_surface_set_buffer_scale(window_->wl.surface, window_->frame.buffer_scale);

/* Postpone binding the buffer until after it's decor has been configured:
 * - Ensure the window is sized properly (with XDG window decorations), see: #113059.
 * - Avoids flickering on startup.
 */
#ifdef WITH_OPENGL_BACKEND
  if (type == GHOST_kDrawingContextTypeOpenGL) {
    window_->backend.egl_window = wl_egl_window_create(
        window_->wl.surface, int(window_->frame.size[0]), int(window_->frame.size[1]));
  }
#endif
#ifdef WITH_VULKAN_BACKEND
  if (type == GHOST_kDrawingContextTypeVulkan) {
    window_->backend.vulkan_window_info = new GHOST_ContextVK_WindowInfo;
    window_->backend.vulkan_window_info->size[0] = window_->frame.size[0];
    window_->backend.vulkan_window_info->size[1] = window_->frame.size[1];
  }
#endif

  /* Drawing context. */
  if (setDrawingContextType(type) == GHOST_kFailure) {
    GHOST_PRINT("Failed to create drawing context" << std::endl);
  }

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    /* Commit needed so the top-level callbacks run (and `toplevel` can be accessed). */
    wl_surface_commit(window_->wl.surface);
    GWL_LibDecor_Window &decor = *window_->libdecor;

    /* Additional round-trip is needed to ensure `xdg_toplevel` is set. */
    wl_display_roundtrip(system_->wl_display_get());

    /* NOTE: LIBDECOR requires the window to be created & configured before the state can be set.
     * Workaround this by using the underlying `xdg_toplevel` */

#  ifdef WITH_VULKAN_BACKEND
    /* A dummy buffer is needed for VULKAN & LIBDECOR,
     * otherwise `decor.initial_configure_seen` and startup locks up. */
    wl_buffer *dummy_buffer = nullptr;
    if (window_->ghost_context_type == GHOST_kDrawingContextTypeVulkan) {
      const uint32_t format = WL_SHM_FORMAT_ARGB8888;
      const int format_size = 4;
      const int buffer_size = (window_->frame.size[0] * window_->frame.size[1]) * format_size;
      const int fd = memfd_create_sealed_for_vulkan_hack("ghost-wl-dummy-buffer");
      ftruncate(fd, buffer_size);
      wl_shm *shm = system_->wl_shm_get();
      wl_shm_pool *pool = wl_shm_create_pool(shm, fd, buffer_size);
      dummy_buffer = wl_shm_pool_create_buffer(pool,
                                               0,
                                               window_->frame.size[0],
                                               window_->frame.size[1],
                                               window_->frame.size[0] * format_size,
                                               format);
      wl_shm_pool_destroy(pool);

      wl_surface_attach(window_->wl.surface, dummy_buffer, 0, 0);
      wl_surface_damage(window_->wl.surface, 0, 0, window_->frame.size[0], window_->frame.size[1]);
      wl_surface_commit(window_->wl.surface);
      ::close(fd);
    }
#  endif /* WITH_GHOST_WAYLAND_LIBDECOR */

    while (!decor.initial_configure_seen) {
      wl_display_flush(system->wl_display_get());
      wl_display_dispatch(system->wl_display_get());
    }

#  ifdef WITH_VULKAN_BACKEND
    if (window_->ghost_context_type == GHOST_kDrawingContextTypeVulkan) {
      wl_surface_attach(window_->wl.surface, nullptr, 0, 0);
      wl_surface_commit(window_->wl.surface);
      wl_buffer_destroy(dummy_buffer);
    }
#  endif /* WITH_GHOST_WAYLAND_LIBDECOR */

    xdg_toplevel *toplevel = libdecor_frame_get_xdg_toplevel(decor.frame);
    gwl_window_state_set_for_xdg(toplevel, state, gwl_window_state_get(window_));
  }
  else
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
  {
    gwl_window_state_set(window_, state);
  }

  /* Commit after setting the buffer.
   * While postponing until after the buffer drawing is context is set
   * isn't essential, it reduces flickering. */
  wl_surface_commit(window_->wl.surface);

  /* Set swap interval to 0 to prevent blocking. */
  setSwapInterval(0);
}

GHOST_WindowWayland::~GHOST_WindowWayland()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

  releaseNativeHandles();

#ifdef WITH_OPENGL_BACKEND
  if (window_->ghost_context_type == GHOST_kDrawingContextTypeOpenGL) {
    wl_egl_window_destroy(window_->backend.egl_window);
  }
#endif
#ifdef WITH_VULKAN_BACKEND
  if (window_->ghost_context_type == GHOST_kDrawingContextTypeVulkan) {
    delete window_->backend.vulkan_window_info;
  }
#endif

  if (window_->xdg.activation_token) {
    xdg_activation_token_v1_destroy(window_->xdg.activation_token);
    window_->xdg.activation_token = nullptr;
  }

  if (window_->wp.fractional_scale_handle) {
    wp_fractional_scale_v1_destroy(window_->wp.fractional_scale_handle);
    window_->wp.fractional_scale_handle = nullptr;
  }

  if (window_->wp.viewport) {
    wp_viewport_destroy(window_->wp.viewport);
    window_->wp.viewport = nullptr;
  }

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    gwl_libdecor_window_destroy(window_->libdecor);
  }
  else
#endif
  {
    gwl_xdg_decor_window_destroy(window_->xdg_decor);
  }

  /* Clear any pointers to this window. This is needed because there are no guarantees
   * that flushing the display will the "leave" handlers before handling events. */
  system_->window_surface_unref(window_->wl.surface);

  wl_surface_destroy(window_->wl.surface);

  /* NOTE(@ideasman42): Flushing will often run the appropriate handlers event
   * (#wl_surface_listener.leave in particular) to avoid attempted access to the freed surfaces.
   * This is not fool-proof though, hence the call to #window_surface_unref, see: #99078. */
  wl_display_flush(system_->wl_display_get());

  delete window_;
}

#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_TSuccess GHOST_WindowWayland::swapBuffers()
{
  GHOST_ASSERT(system_->main_thread_id == std::this_thread::get_id(), "Only from main thread!");
  return GHOST_Window::swapBuffers();
}
#endif /* USE_EVENT_BACKGROUND_THREAD */

GHOST_TSuccess GHOST_WindowWayland::hasCursorShape(GHOST_TStandardCursor cursorShape)
{
  return system_->cursor_shape_check(cursorShape);
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCursorGrab(GHOST_TGrabCursorMode mode)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

  GHOST_Rect bounds_buf;
  GHOST_Rect *bounds = nullptr;
  if (m_cursorGrab == GHOST_kGrabWrap) {
    if (getCursorGrabBounds(bounds_buf) == GHOST_kFailure) {
      getClientBounds(bounds_buf);
    }
    bounds = &bounds_buf;
  }

  if (system_->window_cursor_grab_set(mode,
                                      m_cursorGrab,
                                      m_cursorGrabInitPos,
                                      bounds,
                                      m_cursorGrabAxis,
                                      window_->wl.surface,
                                      this->scale_params_get()))
  {
    return GHOST_kSuccess;
  }
  return GHOST_kFailure;
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCursorShape(GHOST_TStandardCursor shape)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  const GHOST_TSuccess ok = system_->cursor_shape_set(shape);
  m_cursorShape = (ok == GHOST_kSuccess) ? shape : GHOST_kStandardCursorDefault;

  if (ok == GHOST_kSuccess) {
    /* For the cursor to display when the event queue isn't being handled. */
    wl_display_flush(system_->wl_display_get());
  }
  return ok;
}

bool GHOST_WindowWayland::getCursorGrabUseSoftwareDisplay()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return system_->cursor_grab_use_software_display_get(m_cursorGrab);
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCustomCursorShape(
    uint8_t *bitmap, uint8_t *mask, int sizex, int sizey, int hotX, int hotY, bool canInvertColor)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  const GHOST_TSuccess ok = system_->cursor_shape_custom_set(
      bitmap, mask, sizex, sizey, hotX, hotY, canInvertColor);

  if (ok == GHOST_kSuccess) {
    /* For the cursor to display when the event queue isn't being handled. */
    wl_display_flush(system_->wl_display_get());
  }
  return ok;
}

GHOST_TSuccess GHOST_WindowWayland::getCursorBitmap(GHOST_CursorBitmapRef *bitmap)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return system_->cursor_bitmap_get(bitmap);
}

void GHOST_WindowWayland::setTitle(const char *title)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  gwl_window_title_set(window_, title);
}

std::string GHOST_WindowWayland::getTitle() const
{
  /* No need to lock `server_mutex` (WAYLAND never changes this). */
  return window_->title.empty() ? "untitled" : window_->title;
}

void GHOST_WindowWayland::getWindowBounds(GHOST_Rect &bounds) const
{
  getClientBounds(bounds);
}

void GHOST_WindowWayland::getClientBounds(GHOST_Rect &bounds) const
{
  /* No need to lock `server_mutex` (WAYLAND never changes this in a thread). */
  bounds.set(0, 0, UNPACK2(window_->frame.size));
}

GHOST_TSuccess GHOST_WindowWayland::setClientWidth(const uint32_t width)
{
  return setClientSize(width, uint32_t(window_->frame.size[1]));
}

GHOST_TSuccess GHOST_WindowWayland::setClientHeight(const uint32_t height)
{
  return setClientSize(uint32_t(window_->frame.size[0]), height);
}

GHOST_TSuccess GHOST_WindowWayland::setClientSize(const uint32_t width, const uint32_t height)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
  std::lock_guard lock_frame_guard{window_->frame_pending_mutex};
#endif

  GWL_WindowFrame &frame_pending = window_->frame_pending;

  frame_pending.size[0] = width;
  frame_pending.size[1] = height;
  gwl_round_int2_by(frame_pending.size, frame_pending.buffer_scale);

  gwl_window_frame_pending_size_set(window_, nullptr, nullptr, nullptr);

  return GHOST_kSuccess;
}

void GHOST_WindowWayland::screenToClient(int32_t inX,
                                         int32_t inY,
                                         int32_t &outX,
                                         int32_t &outY) const
{
  outX = inX;
  outY = inY;
}

void GHOST_WindowWayland::clientToScreen(int32_t inX,
                                         int32_t inY,
                                         int32_t &outX,
                                         int32_t &outY) const
{
  outX = inX;
  outY = inY;
}

uint16_t GHOST_WindowWayland::getDPIHint()
{
  /* No need to lock `server_mutex`
   * (`outputs_changed_update_scale` never changes values in a non-main thread). */

  if (window_->frame.fractional_scale) {
    return gwl_window_fractional_to_viewport(window_->frame, base_dpi);
  }

  return window_->frame.buffer_scale * base_dpi;
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCursorVisibility(bool visible)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  const GHOST_TSuccess ok = system_->cursor_visibility_set(visible);
  if (ok == GHOST_kSuccess) {
    /* For the cursor to display when the event queue isn't being handled. */
    wl_display_flush(system_->wl_display_get());
  }
  return ok;
}

GHOST_TSuccess GHOST_WindowWayland::setState(GHOST_TWindowState state)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return gwl_window_state_set(window_, state) ? GHOST_kSuccess : GHOST_kFailure;
}

GHOST_TWindowState GHOST_WindowWayland::getState() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return gwl_window_state_get(window_);
}

GHOST_TSuccess GHOST_WindowWayland::invalidate()
{
  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_WindowWayland::setOrder(GHOST_TWindowOrder order)
{
  /* NOTE(@ideasman42): only activation is supported (on X11 & Cocoa for e.g.)
   * both activation and raising is performed. Since WAYLAND only supports activation,
   * do that as the compositor will likely raise the window as well.
   * Although it's not ideal that raising does something but lowering a window is ignored. */
  if (order == GHOST_kWindowOrderTop) {
    gwl_window_activate(window_);
  }

  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_WindowWayland::beginFullScreen() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    libdecor_frame_set_fullscreen(window_->libdecor->frame, nullptr);
  }
  else
#endif
  {
    xdg_toplevel_set_fullscreen(window_->xdg_decor->toplevel, nullptr);
  }

  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_WindowWayland::endFullScreen() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    libdecor_frame_unset_fullscreen(window_->libdecor->frame);
  }
  else
#endif
  {
    xdg_toplevel_unset_fullscreen(window_->xdg_decor->toplevel);
  }
  return GHOST_kSuccess;
}

bool GHOST_WindowWayland::isDialog() const
{
  return window_->is_dialog;
}

GHOST_Context *GHOST_WindowWayland::newDrawingContext(GHOST_TDrawingContextType type)
{
  switch (type) {
    case GHOST_kDrawingContextTypeNone: {
      GHOST_Context *context = new GHOST_ContextNone(m_wantStereoVisual);
      return context;
    }

#ifdef WITH_VULKAN_BACKEND
    case GHOST_kDrawingContextTypeVulkan: {
      GHOST_ContextVK *context = new GHOST_ContextVK(m_wantStereoVisual,
                                                     GHOST_kVulkanPlatformWayland,
                                                     0,
                                                     nullptr,
                                                     window_->wl.surface,
                                                     system_->wl_display_get(),
                                                     window_->backend.vulkan_window_info,
                                                     1,
                                                     2,
                                                     is_debug_context_);
      if (context->initializeDrawingContext()) {
        return context;
      }
      delete context;
      return nullptr;
    }
#endif

#ifdef WITH_OPENGL_BACKEND
    case GHOST_kDrawingContextTypeOpenGL: {
      for (int minor = 6; minor >= 3; --minor) {
        GHOST_Context *context = new GHOST_ContextEGL(
            system_,
            m_wantStereoVisual,
            EGLNativeWindowType(window_->backend.egl_window),
            EGLNativeDisplayType(system_->wl_display_get()),
            EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
            4,
            minor,
            GHOST_OPENGL_EGL_CONTEXT_FLAGS |
                (is_debug_context_ ? EGL_CONTEXT_OPENGL_DEBUG_BIT_KHR : 0),
            GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
            EGL_OPENGL_API);

        if (context->initializeDrawingContext()) {
          return context;
        }
        delete context;
      }
      return nullptr;
    }
#endif

    default:
      /* Unsupported backend. */
      return nullptr;
  }
}

#ifdef WITH_INPUT_IME

void GHOST_WindowWayland::beginIME(int32_t x, int32_t y, int32_t w, int32_t h, bool completed)
{
  system_->ime_begin(this, x, y, w, h, completed);
}

void GHOST_WindowWayland::endIME()
{
  system_->ime_end(this);
}

#endif

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Direct Data Access
 *
 * Expose some members via methods.
 * \{ */

int GHOST_WindowWayland::scale_get() const
{
  return window_->frame.buffer_scale;
}

const GWL_WindowScaleParams &GHOST_WindowWayland::scale_params_get() const
{
  /* NOTE(@ideasman42): This could be kept initialized,
   * since it's such a small struct it's not so important. */
  GWL_WindowScaleParams *scale_params = &window_->scale_params;
  scale_params->is_fractional = (window_->frame.fractional_scale != 0);
  scale_params->scale = scale_params->is_fractional ? window_->frame.fractional_scale :
                                                      window_->frame.buffer_scale;
  return *scale_params;
}

wl_fixed_t GHOST_WindowWayland::wl_fixed_from_window(wl_fixed_t value) const
{
  if (window_->frame.fractional_scale) {
    return gwl_window_fractional_from_viewport(window_->frame, value);
  }
  return value / window_->frame.buffer_scale;
}

wl_fixed_t GHOST_WindowWayland::wl_fixed_to_window(wl_fixed_t value) const
{
  if (window_->frame.fractional_scale) {
    return gwl_window_fractional_to_viewport(window_->frame, value);
  }
  return value * window_->frame.buffer_scale;
}

wl_surface *GHOST_WindowWayland::wl_surface_get() const
{
  return window_->wl.surface;
}

const std::vector<GWL_Output *> &GHOST_WindowWayland::outputs_get()
{
  return window_->outputs;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Window Level Functions
 *
 * High Level Windowing Utilities.
 * \{ */

GHOST_TSuccess GHOST_WindowWayland::close()
{
  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowClose, this));
}

GHOST_TSuccess GHOST_WindowWayland::activate()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
  if (is_main_thread)
#endif
  {
    if (system_->getWindowManager()->setActiveWindow(this) == GHOST_kFailure) {
      return GHOST_kFailure;
    }
  }
  const GHOST_TSuccess success = system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowActivate, this));
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (success == GHOST_kSuccess) {
    if (use_libdecor) {
      /* Ensure there is a swap-buffers, needed for the updated window borders to refresh. */
      notify_decor_redraw();
    }
  }
#endif
  return success;
}

GHOST_TSuccess GHOST_WindowWayland::deactivate()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  /* Actual activation is handled when processing pending events. */
  const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
  if (is_main_thread)
#endif
  {
    system_->getWindowManager()->setWindowInactive(this);
  }
  const GHOST_TSuccess success = system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowDeactivate, this));
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (success == GHOST_kSuccess) {
    if (use_libdecor) {
      /* Ensure there is a swap-buffers, needed for the updated window borders to refresh. */
      notify_decor_redraw();
    }
  }
#endif
  return success;
}

GHOST_TSuccess GHOST_WindowWayland::notify_size()
{
  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowSize, this));
}

GHOST_TSuccess GHOST_WindowWayland::notify_decor_redraw()
{
  /* NOTE: we want to `swapBuffers`, however this may run from a thread and
   * when this windows OpenGL context is not active, so send and update event instead. */
  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowUpdateDecor, this));
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Utility Functions
 *
 * Functionality only used for the WAYLAND implementation.
 * \{ */

void GHOST_WindowWayland::outputs_changed_update_scale_tag()
{
#ifdef USE_EVENT_BACKGROUND_THREAD

  /* NOTE: if deferring causes problems, it could be isolated to the first scale initialization
   * See: #GWL_WindowFrame::is_scale_init. */
  gwl_window_pending_actions_tag(window_, PENDING_OUTPUT_SCALE_UPDATE_DEFERRED);
#else
  outputs_changed_update_scale();
#endif
}

bool GHOST_WindowWayland::outputs_changed_update_scale()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  if (system_->main_thread_id != std::this_thread::get_id()) {
    gwl_window_pending_actions_tag(window_, PENDING_OUTPUT_SCALE_UPDATE);
    return false;
  }
#endif
  int fractional_scale_next = -1;
  int fractional_scale_from_output = 0;

  int scale_next = outputs_max_scale_or_default(outputs_get(), 0, &fractional_scale_from_output);

  if (UNLIKELY(scale_next == 0)) {
    return false;
  }

#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{window_->frame_pending_mutex};
#endif

  if (window_->wp.fractional_scale_handle) {
    /* Let the #wp_fractional_scale_v1_listener::preferred_scale callback handle
     * changes to the windows scale. */
    if (window_->frame_pending.fractional_scale_preferred != 0) {
      fractional_scale_next = window_->frame_pending.fractional_scale_preferred;
      scale_next = fractional_scale_next / FRACTIONAL_DENOMINATOR;
    }
  }

  if (fractional_scale_next == -1) {
    fractional_scale_next = fractional_scale_from_output;
    scale_next = fractional_scale_next / FRACTIONAL_DENOMINATOR;
  }

  bool changed = false;

  const bool is_fractional_prev = window_->frame.fractional_scale != 0;
  const bool is_fractional_next = (fractional_scale_next % FRACTIONAL_DENOMINATOR) != 0;

  /* When non-fractional, never use fractional scaling! */
  window_->frame_pending.fractional_scale = is_fractional_next ? fractional_scale_next : 0;
  window_->frame_pending.buffer_scale = is_fractional_next ?
                                            1 :
                                            fractional_scale_next / FRACTIONAL_DENOMINATOR;

  const int fractional_scale_prev = window_->frame.fractional_scale ?
                                        window_->frame.fractional_scale :
                                        window_->frame.buffer_scale * FRACTIONAL_DENOMINATOR;
  const int scale_prev = fractional_scale_prev / FRACTIONAL_DENOMINATOR;

  /* Resizing implies updating. */
  bool do_frame_resize = false;
  bool do_frame_update = false;

  if (window_->frame_pending.is_scale_init == false) {
    window_->frame_pending.is_scale_init = true;

    /* NOTE(@ideasman42): Needed because new windows are created at their previous pixel-dimensions
     * as the window doesn't save it's DPI. Restore the window size under the assumption it's
     * opening on the same monitor so a window keeps it's previous size on a users system.
     *
     * To support anything more sophisticated, windows would need to be created with a scale
     * argument (representing the scale used when the window was stored, for e.g.). */

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
    if (use_libdecor) {
      /* LIBDECOR needs its own logic. Failing to do this causes the window border
       * not to follow the GHOST window on startup - with multiple monitors,
       * each with different fractional scale, see: #109194.
       *
       * Note that the window will show larger, then resize to be smaller soon
       * after opening. This would be nice to avoid but but would require DPI
       * to be stored in the window (as noted above). */
      int size_next[2] = {0, 0};
      int size_orig[2] = {0, 0};

      /* Leave `window_->frame_pending` as-is, only change the window frame. */
      for (size_t i = 0; i < ARRAY_SIZE(window_->frame_pending.size); i++) {
        const int value = size_next[i] ? window_->frame_pending.size[i] : window_->frame.size[i];
        size_orig[i] = value;
        if (is_fractional_prev || is_fractional_next) {
          size_next[i] = lroundf((value * double(FRACTIONAL_DENOMINATOR)) /
                                 double(fractional_scale_next));
        }
        else {
          size_next[i] = value / scale_prev;
        }
        if (window_->frame_pending.buffer_scale > 1) {
          gwl_round_int_by(&size_next[i], window_->frame_pending.buffer_scale);
        }
      }

      if (size_orig[0] != size_next[0] || size_orig[1] != size_next[1]) {
        GWL_LibDecor_Window &decor = *window_->libdecor;
        libdecor_state *state = libdecor_state_new(UNPACK2(size_next));
        libdecor_frame_commit(decor.frame, state, nullptr);
        libdecor_state_free(state);
      }
    }
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */
    /* Leave `window_->frame_pending` as-is, so changes are detected and updates are applied. */
    do_frame_resize = false;
    do_frame_update = true;
  }
  else {
    /* Test if the scale changed. */
    if ((fractional_scale_prev != fractional_scale_next) ||
        (window_->frame_pending.buffer_scale != window_->frame.buffer_scale))
    {
      do_frame_resize = true;
    }
  }

  if (do_frame_resize) {
    /* Resize the window failing to do so results in severe flickering with a
     * multi-monitor setup when multiple monitors have different scales.
     *
     * NOTE: some flickering is still possible even when resizing this
     * happens when dragging the right hand side of the title-bar in KDE
     * as expanding changed the size on the RHS, this may be up to the compositor to fix. */
    for (size_t i = 0; i < ARRAY_SIZE(window_->frame_pending.size); i++) {
      const int value = window_->frame_pending.size[i] ? window_->frame_pending.size[i] :
                                                         window_->frame.size[i];
      if (is_fractional_prev || is_fractional_next) {
        window_->frame_pending.size[i] = lroundf((value * double(fractional_scale_next)) /
                                                 double(fractional_scale_prev));
      }
      else {
        window_->frame_pending.size[i] = (value * scale_next) / scale_prev;
      }
      if (window_->frame_pending.buffer_scale > 1) {
        gwl_round_int_by(&window_->frame_pending.size[i], window_->frame_pending.buffer_scale);
      }
    }
    do_frame_update = true;
  }

  if (do_frame_update) {
    gwl_window_frame_update_from_pending_no_lock(window_);
    changed = true;
  }

  return changed;
}

bool GHOST_WindowWayland::outputs_enter(GWL_Output *output)
{
  std::vector<GWL_Output *> &outputs = window_->outputs;
  auto it = std::find(outputs.begin(), outputs.end(), output);
  if (it != outputs.end()) {
    return false;
  }
  outputs.push_back(output);
  return true;
}

bool GHOST_WindowWayland::outputs_leave(GWL_Output *output)
{
  std::vector<GWL_Output *> &outputs = window_->outputs;
  auto it = std::find(outputs.begin(), outputs.end(), output);
  if (it == outputs.end()) {
    return false;
  }
  outputs.erase(it);
  return true;
}

#ifdef USE_EVENT_BACKGROUND_THREAD

void GHOST_WindowWayland::pending_actions_handle()
{
  /* Caller must lock `server_mutex`, while individual actions could lock,
   * it's simpler to lock once when handling all window actions. */
  GWL_Window *win = window_;
  GHOST_ASSERT(win->ghost_system->main_thread_id == std::this_thread::get_id(),
               "Run from main thread!");

  gwl_window_pending_actions_handle(win);
}

#endif /* USE_EVENT_BACKGROUND_THREAD */

/** \} */
